# 创建虚拟环境 python -m venv VV
# 激活虚拟环境（PowerShell）
# VV\Scripts\Activate.ps1
import os
import re
import json
import logging
from typing import List, Dict, Any, Union # 添加 Union 导入
from dotenv import load_dotenv

import matplotlib
matplotlib.use('Agg')

# LangChain 核心模块
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
from langchain.prompts.chat import ChatPromptTemplate  # 恢复原始导入路径
from langchain_openai import ChatOpenAI # 兼容 Deepseek API
from langchain_core.runnables import RunnableSequence
from langchain_core.output_parsers import StrOutputParser, JsonOutputParser
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_core.documents import Document
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter

# 新增：导入 ZhipuAI Embeddings (加入智谱AI的向量模型)
from langchain_community.embeddings.openai import OpenAIEmbeddings  # 临时使用OpenAI嵌入

# 修改后（取消注释并修复导入路径）：
from langchain_community.embeddings.zhipuai import ZhipuAIEmbeddings

# 统一使用Pydantic v2导入方式
from pydantic import BaseModel
from langchain_community.vectorstores import FAISS

# 图数据库和可视化
from py2neo import Graph  # 单独导入 Graph
from py2neo.data import Node, Relationship  # 从 data 子模块导入 Node 和 Relationship
import networkx as nx
import matplotlib.pyplot as plt
# from zhipuai import ZhipuAI
from langchain_core.output_parsers import StrOutputParser, JsonOutputParser
from networkx.drawing.layout import arf_layout
from datetime import datetime 
# 新增PDF处理依赖
import fitz
from pdfminer.high_level import extract_text as pdfminer_extract
from datetime import datetime
# 在文件顶部的导入部分添加
import hashlib
from hashlib import sha256
import uuid

import docx
from openpyxl import load_workbook
import xlrd
from datetime import date

class EnhancedKnowledgeBase:
    def __init__(self, project_id: str):

        self.project_id = project_id

        # 重置实例级运行计数器
        self._run_counter = 0  # 从类级改为实例级变量
        # 新增图结构初始化
        self.graph = nx.DiGraph()

        self.existing_ids = set()  # 新增ID追踪集合

        self.logger = logging.getLogger(__name__)  # 新增logger初始化

        # 初始化智谱AI嵌入模型
        self.embeddings = ZhipuAIEmbeddings(
            zhipuai_api_key=os.getenv("ZHIPUAI_API_KEY"),
            timeout=30
        )

        # 初始化 LLM (DeepSeek)
        self.llm = ChatOpenAI(
            model="deepseek-chat",
            api_key=os.getenv("DEEPSEEK_API_KEY"),
            base_url="https://api.deepseek.com/v1",
            timeout=60,
            max_retries=3
        )

          # 新增JSON输出解析器
        self.json_output_parser = JsonOutputParser()

        # 初始化 Neo4j 图数据库
        self.neo4j_graph = Graph(
            os.getenv("NEO4J_URI", "bolt://localhost:7687"),
            auth=('neo4j', "12345678")
        )

          # 初始化时创建项目节点
        self.neo4j_graph.run(
            "MERGE (p:Project {id: $project_id})",
            project_id=project_id
        )

        self._initialize_neo4j_schema()

        # 初始化 FAISS 向量存储（使用已定义的embeddings）
        self.kb = FAISS.from_texts([""], self.embeddings)

    def _initialize_neo4j_schema(self):
        """初始化 Neo4j 数据库的约束和索引，确保数据一致性和查询效率。"""
        try:
            # 确保事件名称唯一 (使用新语法 for Neo4j 4.x/5.x)
            self.neo4j_graph.run("CREATE CONSTRAINT IF NOT EXISTS FOR (e:Event) REQUIRE e.name IS UNIQUE")
            # 为事件描述创建索引以加速查找 (这行语法通常兼容，无需修改)
            self.neo4j_graph.run("CREATE INDEX IF NOT EXISTS FOR (e:Event) ON (e.description)")
            logging.info("Neo4j 索引和约束初始化完成")
        except Exception as e:
            logging.error(f"Neo4j 初始化失败: {e}")
            raise

    def _extract_events_with_relations(self, policy_text: str) -> Dict[str, Any]:
        """
        利用 LLM 从政策文本中提取事件和关系。
        增加 CoT 提示词，引导 LLM 进行推理和补全，并严格控制输出格式。
        """

        # 新增长度检查 ▼▼▼
        MAX_CONTEXT_TOKENS = 60000  # 预留10%安全余量
        system_prompt_tokens = 450   # 实际测量系统提示词token数
        available_tokens = MAX_CONTEXT_TOKENS - system_prompt_tokens
        
        # 动态调整输入长度（按汉字≈2token计算）
        max_policy_chars = int(available_tokens / 2 * 0.9)  # 再预留10%buffer
        if len(policy_text) > max_policy_chars:
            policy_text = policy_text[:max_policy_chars]
            logging.warning(f"动态截断至{max_policy_chars}字符")
        # ▲▲▲ 检查结束 ▲▲▲

        prompt = ChatPromptTemplate.from_messages([
            SystemMessage(
                content="你是一个专业的事件和关系提取器。你的任务是从提供的政策文本中，严格按照以下 JSON 格式提取关键事件（Event）及其之间的逻辑关系（Relationship）。你可能需要基于文本内容进行推理，识别出隐含的逻辑关系，并补全可能缺失的信息。请确保输出只有 JSON，不要包含任何额外文字、前言或后语。"
            ),
            HumanMessage(
                content=f"""
                请从以下政策文本中提取关键事件（Event）及其之间的逻辑关系（Relationship）。
                事件应包含 'name'（事件名称，必填）和可选的 'description'。
                关系应包含 'source'（源事件名称，必填）, 'target'（目标事件名称，必填）和 'type'（关系类型，必填，类型限于：SEQUENCE, CAUSES, CONDITION, CONCURRENT, COMPOSITION），以及可选的 'description'。

                请思考：
                1. 明确识别政策中提到的所有明确事件。
                2. 分析事件之间直接或间接的逻辑联系，例如时间顺序、因果关系、必要条件、并行发生或组成关系。
                3. 如果某个逻辑关系在文本中没有明确说明，但根据常理或业务流程惯例是隐含存在的，请尝试推断并补全，并在关系的description中注明“推断关系”。
                4. 请特别注意，如果文本中提到某个步骤必须在另一个步骤之前或之后，这通常是“SEQUENCE”关系。如果一个事件的发生导致了另一个事件的发生，这是“CAUSES”关系。如果一个事件的发生是另一个事件发生的必要前提，这是“CONDITION”关系。如果两个事件可以同时发生且互不依赖，这是“CONCURRENT”关系。如果一个事件是由多个子事件组成的，这是“COMPOSITION”关系。

                政策文本:
                {policy_text}

                请严格按照以下JSON格式输出，不要输出任何其他内容，确保'events'和'relationships'是顶级键：
                {{
                  "events": [
                    {{"name": "事件A名称", "description": "事件A的描述"}},
                    {{"name": "事件B名称"}}
                  ],
                  "relationships": [
                    {{"source": "事件A名称", "target": "事件B名称", "type": "SEQUENCE", "description": "关系描述，可选"}},
                    {{"source": "事件C名称", "target": "事件D名称", "type": "CAUSES"}}
                  ]
                }}
                """
            )
        ])

        chain = prompt | self.llm 

        raw_llm_output = None # <--- 初始化为 None
        try:
            raw_llm_output = chain.invoke({"policy_text": policy_text})
            logging.info(f"Deepseek LLM 原始输出 (用于调试): {raw_llm_output.content}") # 打印原始输出

            parsed_data = self.json_output_parser.parse(raw_llm_output.content)
            
            # 对解析后的数据进行结构校验和补全
            validated_data = self._validate_event_structure_after_json_parse(parsed_data)
            return validated_data
        except Exception as e:
            logging.error(f"解析LLM输出JSON失败或LLM调用错误: {e}")
            if raw_llm_output: # <--- 只有在 raw_llm_output 被赋值后才尝试访问其 content
                logging.error(f"失败的原始LLM输出内容: {raw_llm_output.content}")
            else:
                logging.error("LLM调用在赋值 raw_llm_output 之前就失败了。")
            return {"events": [], "relationships": []} # 返回空字典以避免后续错误

    def _parse_pdf(self, file_path: str) -> str:
        """通用PDF解析方法，兼容扫描件和数字文档"""
        text = ""
        
        # 方法1: 使用PyMuPDF提取基础文本
        try:
            doc = fitz.open(file_path)
            for page in doc:
                text += page.get_text("text")
            logging.info("PyMuPDF解析成功")
        except Exception as e:
            logging.warning(f"PyMuPDF解析失败: {e}")
        
        # 方法2: 如果方法1失败，使用PDFMiner二次解析
        if not text.strip():
            try:
                text = pdfminer_extract(file_path)
                logging.info("PDFMiner解析成功")
            except Exception as e:
                logging.error(f"PDFMiner解析失败: {e}")
        
        # 处理扫描件（需要用户自行安装Tesseract）
        if not text.strip():
            try:
                text = self._ocr_pdf(file_path)
                logging.info("OCR解析成功")
            except ImportError:
                logging.error("请安装Tesseract OCR: https://github.com/tesseract-ocr/tesseract")

        # 新增智能预处理 ▼▼▼
        # 1. 合并被错误分割的词语（增强版）
        text = re.sub(r'([\u4e00-\u9fa5])-\s+([\u4e00-\u9fa5])', r'\1\2', text)
        # 2. 去除法律条文中的重复条款引用
        text = re.sub(r'（参见第[一二三四五六七八九十零]+条）', '', text)
        # 3. 移除文档页码和页眉页脚
        text = re.sub(r'第\s*[0-9]+\s*页\s*共\s*[0-9]+\s*页', '', text)
        # 4. 合并短段落
        paragraphs = [p.strip() for p in text.split('\n') if p.strip()]
        merged_paragraphs = []
        current_paragraph = ""
        
        for para in paragraphs:
            if len(current_paragraph) + len(para) < 150:  # 合并短段落
                current_paragraph += " " + para
            else:
                if current_paragraph:
                    merged_paragraphs.append(current_paragraph)
                current_paragraph = para
        if current_paragraph:
            merged_paragraphs.append(current_paragraph)
            
        text = '\n'.join(merged_paragraphs)
        # ▲▲▲ 预处理结束 ▲▲▲

        return self._clean_pdf_text(text)

    def _ocr_pdf(self, file_path: str) -> str:
        """处理扫描版PDF"""
        import pytesseract
        from PIL import Image
        
        text = ""
        doc = fitz.open(file_path)
        for page_num in range(len(doc)):
            page = doc.load_page(page_num)
            pix = page.get_pixmap()
            img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
            text += pytesseract.image_to_string(img, lang='chi_sim+eng')
        return text

    def _clean_pdf_text(self, text: str) -> str:
        """清洗PDF解析结果"""
        # 合并被错误分割的词语
        text = re.sub(r'(\w+)-\s+(\w+)', r'\1\2', text)
        # 去除页眉页脚
        text = re.sub(r'\d{1,3}\s?/\s?\d{1,3}', '', text)
        # 标准化空白字符
        return re.sub(r'\s+', ' ', text).strip()

    def _parse_docx(self, file_path):
        try:
            doc = docx.Document(file_path)
            # 提取段落文本和表格内容
            content = []
            for para in doc.paragraphs:
                content.append(para.text)
            for table in doc.tables:
                for row in table.rows:
                    cells = [cell.text for cell in row.cells]
                    content.append('\t'.join(cells))
            return '\n'.join(content)
        except Exception as e:
            self.logger.error(f"DOCX解析失败: {str(e)}")
            return ""

    def _parse_excel(self, file_path):
        """解析Excel文档（支持xls和xlsx）"""
        content = []
        try:
            # if file_path.lower().endswith('.xlsx'):
            #     wb = load_workbook(filename=file_path, data_only=True)
            #     for sheet in wb:
            #         for row in sheet.iter_rows(values_only=True):
            #             # 增强空值处理和类型转换
            #             cleaned_row = [
            #                 str(cell) if not isinstance(cell, (datetime, date)) 
            #                 else cell.strftime('%Y-%m-%d')
            #                 for cell in row
            #             ]
            #             content.append('\t'.join(cleaned_row))
            # else:
                wb = xlrd.open_workbook(file_path)
                for sheet in wb.sheets():
                    for row in range(sheet.nrows):
                        content.append('\t'.join(map(str, sheet.row_values(row))))
                #此处的return 应该是往前缩进一格，与前面注释了的if else语句
                return '\n'.join(content)
        except Exception as e:
            self.logger.error(f"Excel解析失败: {str(e)}")
            return ""

    def _parse_doc(self, file_path):
        try:
            import win32com.client
            word = win32com.client.Dispatch("Word.Application")
            word.Visible = False  # <-- 新增隐藏窗口
            doc = word.Documents.Open(file_path)
            text = doc.Content.Text
            doc.Close()
            word.Quit()
            return text
        except Exception as e:
            self.logger.error(f"DOC解析失败: {str(e)}")
            return ""

    def _parse_txt(self, file_path):
        try:
            # import chardet
            with open(file_path, 'rb') as f:
                raw_data = f.read()
            
            # 使用chardet检测编码
                result = chardet.detect(raw_data)
                detected_encoding = result['encoding']
            
            # 处理常见编码别名
                encoding_mapping = {
                    'GB2312': 'GB18030',
                    'Big5-HKSCS': 'Big5',
                    'ISO-8859-1': 'latin1'
                }
                final_encoding = encoding_mapping.get(detected_encoding, detected_encoding)
            
            # 优先尝试检测到的编码
                try:
                    return raw_data.decode(final_encoding, errors='replace')
                except (LookupError, UnicodeDecodeError):
                    self.logger.warning(f"检测编码 {final_encoding} 解码失败，尝试备用编码")
                
        except ImportError:
            self.logger.warning("未安装chardet，使用基础编码检测")
        except Exception as e:
            self.logger.error(f"TXT解析异常: {str(e)}")
        # 备用编码列表
        encodings = ['utf-8', 'gbk', 'gb18030', 'big5', 'utf-16', 'latin1']
        for encoding in encodings:
            try:
                with open(file_path, 'r', encoding=encoding, errors='replace') as f:
                    return f.read()
            except UnicodeDecodeError:
                continue
            except Exception as e:
                self.logger.error(f"TXT解析失败 [{encoding}]: {str(e)}")
        self.logger.error(f"无法解码TXT文件，尝试过的编码: {encodings}")
        return ""

    def parse_file(self, file_path):
        """统一文件解析入口（不区分大小写）"""
        ext = os.path.splitext(file_path)[1].lower()
        
        if ext == '.pdf':
            return self._parse_pdf(file_path)
        # elif ext == '.docx':
        #     return self._parse_docx(file_path)
        elif ext in ('.xls', '.xlsx'):
            return self._parse_excel(file_path)
        elif ext == '.txt':
            # with open(file_path, 'r', encoding='utf-8') as f:
            #     return f.read()
            return self._parse_txt(file_path)
        # elif ext == '.doc':
        elif ext in ('.doc', '.docx'):
            # 保留原有DOC解析逻辑
            return self._parse_doc(file_path)  
        else:
            self.logger.warning(f"不支持的格式: {ext}")
            return ""

    def _validate_event_structure_after_json_parse(self, data: Dict[str, Any]) -> Dict[str, Any]:
        """校验并补全 LLM 提取的事件和关系结构。"""
        if not isinstance(data, dict):
            logging.warning(f"LLM输出的顶层结构不是字典: {data}")
            return {"events": [], "relationships": []}

        events = data.get("events", [])
        relationships = data.get("relationships", [])

        if not isinstance(events, list):
            logging.warning(f"LLM输出的'events'不是列表: {events}")
            events = []
        if not isinstance(relationships, list):
            logging.warning(f"LLM输出的'relationships'不是列表: {relationships}")
            relationships = []

        # 校验并补全事件
        cleaned_events = []
        for event in events:
            if isinstance(event, dict) and "name" in event:
                event["description"] = event.get("description", event["name"]) # 默认description为name
                cleaned_events.append(event)
            else:
                logging.warning(f"发现一个格式不正确的事件，已跳过: {event}")

        # 校验并补全关系
        cleaned_relationships = []
        valid_relation_types = {"SEQUENCE", "CAUSES", "CONDITION", "CONCURRENT", "COMPOSITION"}
        for rel in relationships:
            if (isinstance(rel, dict) and
                "source" in rel and "target" in rel and "type" in rel and
                rel["type"] in valid_relation_types):
                rel["description"] = rel.get("description", "") # 默认description为空字符串
                cleaned_relationships.append(rel)
            else:
                logging.warning(f"发现一个格式不正确或类型无效的关系，已跳过: {rel}")

        return {"events": cleaned_events, "relationships": cleaned_relationships}

    def _create_event_node(self, name: str, description: str):
        """创建事件节点，如果已存在则更新其描述。"""
        # node = Node("Event", name=name, description=description, id=f"Event_{name}")
        # self.neo4j_graph.merge(node, ("Event", "name"))
        # # 创建BELONGS_TO关系到项目节点
        # project_node = Node("Project", id=self.project_id)
        # self.neo4j_graph.merge(Relationship(node, "BELONGS_TO", project_node), ("Project", "id"))

        query = """
        MERGE (e:Event {name: $name})
        ON CREATE SET e.description = $description, e.id = $id
        ON MATCH SET e.description = $description
        """
        self.neo4j_graph.run(query, name=name, description=description, id=f"Event_{name}")
        # 创建BELONGS_TO关系到项目节点
        self.neo4j_graph.run(
            "MERGE (p:Project {id: $project_id})",
            project_id=self.project_id
        )
        self.neo4j_graph.run(
            "MATCH (e:Event {name: $name}), (p:Project {id: $project_id}) "
            "MERGE (e)-[r:BELONGS_TO]->(p)",
            name=name, project_id=self.project_id
        )

        logging.info(f"成功存储事件: {name}")

    def _create_relation(self, source_name: str, target_name: str, rel_type: str, description: str, tx):
        """创建事件之间的关系。"""
        # 直接在事务中查找或创建源节点和目标节点，并创建关系
        query = """
        MATCH (s:Event {name: $source_name}), (t:Event {name: $target_name})
        MERGE (s)-[r:%s]->(t)
        SET r.description = $description
        RETURN type(r) AS relation_type, r.description AS relation_details
        """ % rel_type
        try:
            tx.run(query, 
                source_name=source_name,
                target_name=target_name,
                description=description)
            logging.info(f"成功创建关系: {source_name} -[{rel_type}]-> {target_name}")
        except Exception as e:
            logging.error(f"创建关系失败: {source_name} -[{rel_type}]-> {target_name} - {e}")

    # 新增
    def _merge_results(self, existing: Dict, new: Dict) -> Dict:
        """合并多次提取的结果，自动去重"""
        seen_events = {(e["name"], e.get("description")) for e in existing["events"]}
        seen_rels = {(r["source"], r["target"], r["type"]) for r in existing["relationships"]}
        
        # 合并事件
        for event in new.get("events", []):
            key = (event["name"], event.get("description"))
            if key not in seen_events:
                existing["events"].append(event)
                seen_events.add(key)
        
        # 合并关系
        for rel in new.get("relationships", []):
            key = (rel["source"], rel["target"], rel["type"])
            if key not in seen_rels:
                existing["relationships"].append(rel)
                seen_rels.add(key)
        
        return existing

    def add_policy_document(self, policy_text: str) -> Dict[str, Any]:
        """
        从政策文本中提取事件和关系，并存储到知识图谱和向量库。
        """
        logging.info("开始提取政策文档中的事件和关系...")

        # ========== 新增智能分割处理 ==========
        text_splitter = RecursiveCharacterTextSplitter(
            chunk_size=2000,
            chunk_overlap=300,
            separators=["\n\n第[一二三四五六七八九十]+章", "。", "；", "\n•\t", "\n"]
        )
        split_documents = text_splitter.create_documents([policy_text])
        filtered_chunks = []
        
        for chunk in split_documents:
            clean_chunk = re.sub(r'\s{2,}', ' ', chunk.page_content)
            if 50 < len(clean_chunk) < 2500:
                filtered_chunks.append(clean_chunk)
        
        BATCH_SIZE = 2
        results = {"events": [], "relationships": []}
        
        for i in range(0, len(filtered_chunks), BATCH_SIZE):
            batch = filtered_chunks[i:i+BATCH_SIZE]
            combined_batch = "\n[文档块分割线]\n".join(batch)
            
            if len(combined_batch) > 5000:
                combined_batch = combined_batch[:5000] + "\n[内容已截断]"  #后续添加
                sub_batches = text_splitter.split_text(combined_batch)
                for sub in sub_batches:
                    extracted = self._extract_events_with_relations(sub)
                    results = self._merge_results(results, extracted)
            else:
                extracted = self._extract_events_with_relations(combined_batch)
                results = self._merge_results(results, extracted)
        # ========== 新增代码结束 ==========

        extracted_data = self._extract_events_with_relations(policy_text)

        events = extracted_data.get("events", [])
        relationships = extracted_data.get("relationships", [])

        if not events and not relationships:
            logging.warning("从政策文档中未提取到任何事件或关系。请检查LLM输出和解析逻辑。")
            return {"events": [], "relationships": []}

        # 存储事件到 Neo4j 和 FAISS
        # 在一个事务中批量处理事件的创建
        event_nodes_created = 0
        for event in events:
            event_name = event.get("name")
            event_description = event.get("description", event_name) # 使用名称作为默认描述
            if event_name:
                self._create_event_node(event_name, event_description)
                # 生成唯一ID：项目ID+事件名称+描述的哈希值 ▼▼▼
                desc_hash = sha256(event_description.encode()).hexdigest()[:8]
                unique_id = f"{self.project_id}_{event_name}_{desc_hash}_{uuid.uuid4().hex[:6]}"
                # 存储到FAISS
                # vector_id = event_name # FAISS 的 ID 可以是事件名称
                # self.kb.add_texts([event_name], metadatas=[{"description": event_description}], ids=[vector_id])
                
                # 在add_texts前添加检查
                if unique_id in self.existing_ids:
                    logging.warning(f"发现重复ID尝试: {unique_id}")
                    continue
                self.existing_ids.add(unique_id)
                self.kb.add_texts([event_name], metadatas=[{"description": event_description}],ids=[unique_id])
                event_nodes_created += 1
            else:
                logging.warning(f"发现一个没有'name'的事件：{event}")
        logging.info(f"成功存储 {event_nodes_created} 个事件到知识图谱和向量库")


        # 存储关系到 Neo4j
        tx = self.neo4j_graph.begin()
        relations_added_count = 0
        for rel in relationships:
            source = rel.get("source")
            target = rel.get("target")
            rel_type = rel.get("type")
            rel_description = rel.get("description", "")
            if source and target and rel_type:
                try:
                    self._create_relation(source, target, rel_type, rel_description, tx)
                    relations_added_count += 1
                except Exception as e:
                    logging.error(f"创建关系失败: {rel} - {e}")
            else:
                logging.warning(f"发现一个不完整的关系：{rel}")
        
        if relations_added_count > 0:
            self.neo4j_graph.commit(tx)
            logging.info(f"成功存储 {relations_added_count} 个关系到知识图谱。")
        else:
            self.neo4j_graph.rollback(tx) # 如果没有成功添加关系，回滚事务
            logging.info(f"未存储任何关系到知识图谱。")

        return extracted_data # 返回提取到的事件和关系

    def _get_event_node(self, step: str) -> str | None:
        """
        从图谱中查找与给定步骤匹配的事件节点。
        优先通过 'name' 精确匹配，如果找不到，再尝试 'description' 精确匹配。
        未来这里应该集成 FAISS 进行语义匹配。
        """
        # 尝试通过 name 属性精确匹配
        query_by_name = f"""
        MATCH (e:Event)
        WHERE e.name = '{step}'
        RETURN e.name AS event_name, e.description AS event_description
        LIMIT 1
        """
        result_by_name = self.neo4j_graph.run(query_by_name).data()
        if result_by_name:
            logging.info(f"通过名称找到事件节点: {result_by_name[0]['event_name']} (描述: {result_by_name[0]['event_description']})")
            return result_by_name[0]["event_name"] # 返回用于匹配的 name 属性

        # 如果通过 name 未找到，尝试通过 description 属性精确匹配
        query_by_description = f"""
        MATCH (e:Event)
        WHERE e.description = '{step}'
        RETURN e.name AS event_name, e.description AS event_description
        LIMIT 1
        """
        result_by_description = self.neo4j_graph.run(query_by_description).data()
        if result_by_description:
            logging.info(f"通过描述找到事件节点: {result_by_description[0]['event_name']} (描述: {result_by_description[0]['event_description']})")
            return result_by_description[0]["event_name"] # 返回用于匹配的 name 属性

        logging.warning(f"未找到对应事件节点（名称或描述均不匹配）: '{step}'")
        return None

    def _get_actual_relations(self, source: str, target: str) -> set:
        """获取图谱中实际存在的源事件和目标事件之间的关系类型。"""
        query = f"""
        MATCH (a:Event {{name: '{source}'}})-[r]->(b:Event {{name: '{target}'}})
        RETURN type(r) AS relation_type
        """
        result = self.neo4j_graph.run(query).data()
        actual_relations = {r["relation_type"] for r in result}
        logging.info(f"实际关系从 '{source}' 到 '{target}': {actual_relations}")
        return actual_relations

    def _get_required_relations(self, source: str, target: str) -> set:
        """获取政策要求的必要关系。这是一个示例，未来将扩展为动态规则引擎。"""
        required = set()
        # 示例政策规则：如果“付款操作”目标事件与“合同签署”源事件相关，则要求“SEQUENCE”和“CONDITION”关系。
        if "付款操作" in target and "合同签署" in source: # 这里可以改为更复杂的规则
            required.add("SEQUENCE")
            required.add("CONDITION")
            logging.info(f"规则检测：'{source}'到'{target}'要求关系: {{'SEQUENCE', 'CONDITION'}}")
        
        # 更多规则...
        if "发票验证通过" in source and "付款操作" in target:
            required.add("CONDITION")
            logging.info(f"规则检测：'{source}'到'{target}'要求关系: {{'CONDITION'}}")
        
        # 如果是“资质审核通过”到“货物验收”
        if "货物验收" in target and "资质审核通过" in source:
            required.add("CONDITION")
            logging.info(f"规则检测：'{source}'到'{target}'要求关系: {{'CONDITION'}}")

        # 如果是“验收合格”到“发票收到”
        if "发票收到" in target and "验收合格" in source:
            required.add("SEQUENCE")
            logging.info(f"规则检测：'{source}'到'{target}'要求关系: {{'SEQUENCE'}}")

        # 如果是“发票收到”到“发票验证”
        if "发票验证" in target and "发票收到" in source:
            required.add("SEQUENCE")
            logging.info(f"规则检测：'{source}'到'{target}'要求关系: {{'SEQUENCE'}}")

        return required

    # 新增节点添加方法 ▼▼▼▼
    def add_node(self, step_name):
        # 终极防重复节点ID生成策略（最终版）
        from hashlib import blake2b
        import threading
        
        # 使用BLAKE2哈希算法（抗碰撞性更强）
        raw_hash = blake2b(step_name.encode('utf-8'), digest_size=16).hexdigest()
        # 生成复合ID：哈希+完整UUID+微秒时间戳+进程ID+线程ID
        timestamp = datetime.now().strftime("%f%S%M%H%d%m%Y") 
        process_id = os.getpid()
        thread_id = threading.get_native_id()
        
        # 新增硬件级唯一标识 ▼▼▼增加MAC地址异常处理
        node = uuid.getnode()
        if node == 0:
            node = int.from_bytes(os.urandom(6), byteorder='big')  # 生成随机MAC作为fallback
        mac_address = ':'.join(("%012X" % node)[i:i+2] for i in range(0, 12, 2))
        
        # 新增运行次数字段 ▼▼▼
        if not hasattr(self, '_run_counter'):
            self._run_counter = 0
        self._run_counter += 1
        
        # 修改ID生成公式 ▼▼▼
        node_id = f"{raw_hash}-{uuid.uuid4()}-{timestamp}-PID{process_id}-TID{thread_id}-RUN{self._run_counter}-MAC{mac_address}"
        
        # 增加调试日志 ▼▼▼
        logging.debug(f"生成节点ID要素｜步骤：{step_name}｜原始哈希：{raw_hash}｜完整ID：{node_id}")
        
        if node_id in self.graph:
            logging.critical(f"发现重复节点ID！步骤：{step_name}｜哈希：{raw_hash}｜完整ID：{node_id}")
            # 在异常中包含调试信息
            raise ValueError(
                f"节点ID生成异常，请联系系统管理员。"
                f"冲突步骤：{step_name}\n"
                f"哈希值：{raw_hash}\n"
                f"完整ID：{node_id}"
            )
            
        self.graph.add_node(node_id, label=step_name[:50])
        return node_id

    def normalize_step(self, step: str) -> str:
        """标准化流程步骤名称（增强防冲突版）"""
        # 防御性处理无效输入
        if not isinstance(step, str) or not step.strip():
            return "INVALID_STEP"

        # 保存原始步骤用于哈希生成和日志记录
        original_step = step
            
        # 增强清理逻辑（新增更多过滤规则）
        step = re.sub(r'[^a-zA-Z0-9\u4e00-\u9fa5]', '', step)
        step = re.sub(r'^[A-Z]+-\d+\s*', '', step)
        step = re.sub(r'^(?:步骤|阶段|环节)\s*', '', step)  # 新增：去除前缀
        
        # 强化语义标准化规则（新增更多模式）
        replacements = {
            r'^项目名称$': '项目名称字段',  # 精确匹配项目名称
            r'^合同名称$': '合同名称字段',
            r'^采购名称$': '采购名称字段',
            # ... 其他规则保持不变 ...
        }

        # 强化语义标准化规则（新增更多模式）
        replacements.update({
            r'^(项目|合同|采购)?名称$': '通用名称字段',
            r'^(金额|数额)$': '金额字段',
            r'^(日期|时间)$': '时间字段'
        })
        
        # 使用完全匹配代替部分匹配
        for pattern, replacement in replacements.items():
            if re.fullmatch(pattern, step):
                step = replacement
                break
        
        # 构建最终结果（保留语义+哈希）
        normalized = step.strip()[:25]  # 限制语义部分长度

        # 强化哈希生成要素
        raw_hash = hashlib.sha3_256(
            f"{original_step}|{step}|{datetime.now().timestamp()}|{os.getpid()}".encode()
        ).hexdigest()[:16]  # 改用更安全的哈希算法并增加长度

        result = f"{normalized}-{raw_hash}"
        
        # 增强日志记录
        logging.debug(f"标准化详情 | 原始: {original_step} | 清洗后: {step} | 最终: {result}")
        return result

    def validate_workflow(self, workflow_steps: List[str]) -> tuple[List[str], str]:
        """
        验证业务流程的合规性。
        它将用户输入的业务流程步骤序列与知识图谱中的逻辑关系进行比对，
        并识别出违规情况。
        """

        # 新增标准化处理 ▼▼▼
        workflow_steps = [self.normalize_step(step) for step in workflow_steps]

        # 新增步骤去重检查 ▼▼▼
        seen_steps = set()
        unique_steps = []
        for step in workflow_steps:
            if step not in seen_steps:
                seen_steps.add(step)
                unique_steps.append(step)
            else:
                logging.warning(f"发现重复流程步骤: {step}")
        workflow_steps = unique_steps
        # ▲▲▲▲▲

        violations = []
        event_chain_description = []

        # 检查Neo4j是否有任何节点，这里使用一个更可靠的方法来检查图谱是否为空
        # 查询是否有任何Event节点，如果不存在，则图谱可能未成功初始化
        node_count_query = "MATCH (e:Event) RETURN count(e) AS count"
        node_count_result = self.neo4j_graph.run(node_count_query).data()
        if not node_count_result or node_count_result[0]['count'] == 0:
             logging.warning("图谱中没有提取到任何节点或关系，请检查数据导入是否成功。")
             violations.append("知识图谱为空，无法进行流程验证。请确认政策文档已成功处理并存储。")
             return violations, "无法构建事件链。"

        # 映射工作流步骤到图谱中的事件节点
        mapped_events = []
        for step in workflow_steps:
            # node_name = self._get_event_node(step)
            node_name = self.add_node(step)
            if node_name:
                mapped_events.append(node_name)
            else:
                violations.append(f"未找到对应事件节点: {step}")
                # 即使没有找到节点，也尝试继续，以便发现其他违规
                mapped_events.append(None) # 使用 None 占位符，以便后续处理


        # 构建和验证事件链
        for i in range(len(mapped_events) - 1):
            source_event = mapped_events[i]
            target_event = mapped_events[i+1]

            if source_event is None or target_event is None:
                # 之前已经报了“未找到对应事件节点”的错，这里跳过当前这个关系对的检查
                continue

            event_chain_description.append(f"{source_event} → {target_event}")

            # 1. 检查是否存在路径（可选，但通常有助于理解流程连通性）
            path_query = f"""
            MATCH p=(a:Event {{name: '{source_event}'}})-[*]->(b:Event {{name: '{target_event}'}})
            RETURN count(p) > 0 AS path_exists
            """
            path_exists = self.neo4j_graph.run(path_query).data()[0]["path_exists"]
            if not path_exists:
                violations.append(f"流程连通性违规: '{source_event}' 到 '{target_event}' 之间没有有效路径。")

            # 2. 检查实际关系与要求关系
            actual_rels = self._get_actual_relations(source_event, target_event)
            required_rels = self._get_required_relations(source_event, target_event)

            # 检查所有要求关系是否都存在于实际关系中
            missing_rels = required_rels - actual_rels
            if missing_rels:
                violations.append(f"缺失强制逻辑关系: '{source_event}' 到 '{target_event}' 之间缺失类型为 {missing_rels} 的关系。")
            
            # 检查是否存在不期望的（但未被明确禁止的）关系，这里仅记录，不作为违规
            unexpected_rels = actual_rels - required_rels
            if unexpected_rels:
                logging.info(f"存在非强制但存在的逻辑关系: '{source_event}' 到 '{target_event}' 之间存在类型为 {unexpected_rels} 的关系。")


        final_event_chain_description = " → ".join([e for e in mapped_events if e is not None]) # 只连接成功映射的事件
        
        # 检查是否所有步骤都成功映射
        validity = True
        if any(event is None for event in mapped_events):
            validity = False # 至少有一个步骤未找到
        
        if violations: # 如果有任何违规，则流程无效
            validity = False

        return violations, f"有效性: {validity}\n事件链: {final_event_chain_description}"


    def visualize_graph(self):
        """
        从 Neo4j 数据库中提取数据并使用 NetworkX 和 Matplotlib 进行可视化。
        """
        # 新增：清除之前的所有图形
        plt.close('all')  # 关闭所有已打开的matplotlib图形
        # 初始化全新的图形对象
        plt.figure(figsize=(12, 8)).clf()

        # 新增数据检查 ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼
        # node_count = self.neo4j_graph.evaluate("MATCH (n:Event) RETURN count(n)")
        # if node_count == 0:
        #     logging.error("无法生成图表：知识图谱中没有事件节点")
        #     return "no_graph_available.png"
        # ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲

         # 新增字体配置（在方法顶部添加）
        plt.rcParams['font.sans-serif'] = ['SimHei']  # 设置中文字体
        plt.rcParams['axes.unicode_minus'] = False   # 解决负号显示问题

        # 创建图片存储目录
        graph_dir = os.path.join("uploads", "graphs", f"project_{self.project_id}")
        os.makedirs(graph_dir, exist_ok=True)
    
        # 生成唯一文件名
        timestamp = int(datetime.now().timestamp())
        graph_filename = f"workflow_graph_{timestamp}.png"
        graph_path = os.path.join(graph_dir, graph_filename)
        # 修改查询，确保返回所有事件的描述，包括没有关系的孤立事件
        # 这次我们将获取所有事件，并使用OPTIONAL MATCH获取关系
        query = f"""
        MATCH (e:Event)-[:BELONGS_TO]->(p:Project {{id: $project_id}})
        OPTIONAL MATCH (e)-[r]->(d:Event)
        RETURN e.name AS source_name, 
               d.name AS target_name,
               type(r) AS relation_type,
               r.description AS relation_details
        """

        results = self.neo4j_graph.run(query, project_id=self.project_id).data()
        G = nx.DiGraph()

        if not results:
            logging.warning("图谱中没有可用于可视化的节点或关系。请确认数据已成功导入。")
            return

        # 用于存储已添加的节点，避免重复添加，同时确保所有节点都被添加
        added_nodes = set()
        
        for record in results:
            source_name = record["source_name"]  # 使用正确的字段名
            target_name = record["target_name"]
            rel_type = record["relation_type"]
            rel_details = record["relation_details"]

            # 添加源节点（使用事件名称）
            if source_name and source_name not in added_nodes:
                G.add_node(source_name)
                added_nodes.add(source_name)

            # 添加目标节点（如果存在）
            if target_name and target_name not in added_nodes:
                G.add_node(target_name)
                added_nodes.add(target_name)
                
            # 添加边（如果存在关系）
            if source_name and target_name and rel_type:
                edge_label = f"{rel_type}\n({rel_details})" if rel_details else rel_type
                G.add_edge(source_name, target_name, label=edge_label)

        # 新增节点度数过滤（在绘制前添加）
        node_degrees = dict(G.degree())
        min_degree = 2  # 只显示至少有两个连接的节点
        significant_nodes = [n for n, degree in node_degrees.items() if degree >= min_degree]
        H = G.subgraph(significant_nodes)

        # if not G.nodes():
        #     logging.warning("图谱中没有可用于可视化的节点。请确认数据已成功导入。")
        #     return
        if not H.nodes():
            logging.warning("过滤后无显著节点可显示")
            H = G  # 回退到显示全部节点
            if not H.nodes():
                logging.error("图谱数据异常：存在节点但无法构建有效关系")
                # 生成错误提示图并返回路径
                plt.figure(figsize=(8, 4))
                plt.text(0.5, 0.5, '图谱数据异常\n请联系管理员', 
                        ha='center', va='center', fontsize=14)
                plt.axis('off')
                plt.savefig(graph_path, bbox_inches='tight')
                plt.close()
                return graph_path

        # 调整布局参数
        # plt.figure(figsize=(15, 10)) # 适当增大图幅
        # pos = nx.spring_layout(G, k=0.7, iterations=50) # 调整布局参数
        pos = arf_layout(H)  # 使用更高级的自动布局算法

        # pos = nx.kamada_kawai_layout(H)
        # 暂时注释
        # pos = nx.spring_layout(H, k=1.5, iterations=100, seed=42, scale=3.0, dim=2, pos=None, fixed=None, weight='weight', center=None)  # 减小k值使节点更紧凑
        # ==== 新增边缘节点分散处理 ====
        min_x = min(pos[node][0] for node in H.nodes())
        max_x = max(pos[node][0] for node in H.nodes())
        min_y = min(pos[node][1] for node in H.nodes())
        max_y = max(pos[node][1] for node in H.nodes())
        
        for node in H.nodes():
            x, y = pos[node]
            edge_force = 0.05
            if x < min_x + 0.1*(max_x-min_x):
                pos[node][0] -= edge_force
            elif x > max_x - 0.1*(max_x-min_x):
                pos[node][0] += edge_force
            if y < min_y + 0.1*(max_y-min_y):
                pos[node][1] -= edge_force
            elif y > max_y - 0.1*(max_y-min_y):
                pos[node][1] += edge_force
        # ==============================

        # 新增边过滤逻辑 ▼▼▼
        # significant_edges = [
        #     (u, v) for u, v, d in H.edges(data=True) 
        #     if d.get('label') in ['SEQUENCE', 'CONDITION']  # 只保留关键关系类型
        # ]
        # H = nx.DiGraph()
        # H.add_edges_from(significant_edges)
        # ▲▲▲▲▲

        # 修改绘制部分使用子图H
        nx.draw_networkx_nodes(H, pos, node_color='skyblue', node_size=800, alpha=0.9, linewidths=0.5)
        # 对应修改其他绘制调用
        node_labels = {node: node for node in H.nodes()}
        nx.draw_networkx_labels(H, pos, labels=node_labels, font_size=8, font_weight='bold')
        # 边相关绘制也改用H
        nx.draw_networkx_edges(H, pos, edge_color='gray', arrows=True, arrowsize=20,width=2, alpha=0.9,connectionstyle="arc3,rad=0.1")
        edge_labels = nx.get_edge_attributes(H, "label")
        nx.draw_networkx_edge_labels(H, pos, edge_labels=edge_labels,font_color='red', font_size=9, bbox={"alpha":0})


        # 绘制节点
        # nx.draw_networkx_nodes(G, pos, node_color='skyblue', node_size=3000, alpha=0.9)
        
        # # 绘制节点标签 (使用节点描述作为标签)
        # node_labels = {node: node for node in G.nodes()}
        # nx.draw_networkx_labels(G, pos, labels=node_labels, font_size=9, font_weight='bold', font_color='black')

        # # 绘制边
        # nx.draw_networkx_edges(G, pos, edge_color='gray', arrows=True, arrowsize=20, width=1.5, alpha=0.7)
        
        # # 绘制边标签
        # edge_labels = nx.get_edge_attributes(G, "label") # 从边的属性中获取 'label'
        # nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels, font_color='red', font_size=7, bbox={"alpha":0})

        plt.title("事件与逻辑关系图谱", fontsize=18)
        plt.axis('off')
        plt.tight_layout() # 自动调整，避免标签重叠
        # plt.show()

        # 保存图片到指定路径
        # plt.savefig(graph_path, bbox_inches='tight', dpi=300)
        # logging.info(f"图表已保存至：{os.path.abspath(graph_path)}")

        # 强制生成最终图表（新增保障机制）
        try:
            plt.savefig(graph_path, bbox_inches='tight', dpi=300)
            logging.info(f"图表已保存至：{os.path.abspath(graph_path)}")
        except Exception as e:
            logging.error(f"图表保存失败: {str(e)}")
            graph_path = os.path.join(graph_dir, "fallback_graph.png")
            plt.savefig(graph_path)  # 强制保存到固定路径
        
        # 最终保障机制（新增）
        if not os.path.exists(graph_path):
            logging.warning("触发最终保障机制生成基础流程图")
            plt.figure(figsize=(8, 4))
            plt.text(0.5, 0.5, '业务流程逻辑图', 
                    ha='center', va='center', fontsize=14)
            plt.axis('off')
            plt.savefig(graph_path, bbox_inches='tight')
            plt.close()
        return graph_path
        
    # 审计报告
    def generate_audit_report(self, violations: List[str], validation_report: str) -> str:
        """
        生成结构化审计报告（增强版）
        新增功能：
        1. 违规等级分类（严重/警告）
        2. 流程步骤可视化支持
        3. 自动生成合规建议
        """
        # 生成报告头信息 ▼▼▼
        report_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        report_title = "智能审计报告"
        audit_scope = "业务流程合规性审计"
        
        # 违规分析
        critical_violations = [v for v in violations if "强制" in v]
        warning_violations = [v for v in violations if "强制" not in v]
        
        # 生成详细分析 ▼▼▼
        process_steps_analysis = [
            "1. 合同签署 → 供应商资质审核 → 货物验收 → 发票验证 → 付款操作 → 银行转账完成",
            "2. 完整流程应包含6个标准步骤和5个必要关系"
        ]
        
        violation_analysis = {
            "critical": {"count": len(critical_violations), "examples": critical_violations[:3]},
            "warning": {"count": len(warning_violations), "examples": warning_violations[:3]}
        }
        
        report_content = f"""
        # {report_title}
    
        **生成时间**: {report_time}  
        **审计范围**: {audit_scope}
    
        ## 关键审计指标
        - 总违规项: `{len(violations)}` 项
        - 严重违规: <span style="color:red">{violation_analysis['critical']['count']}</span> 项
        - 警告事项: <span style="color:orange">{violation_analysis['warning']['count']}</span> 项
    
        ## 流程步骤分析
        ```mermaid
        graph TD
            A[合同签署] --> B[供应商资质审核]
            B --> C[货物验收]
            C --> D[发票验证]
            D --> E[付款操作]
            E --> F[银行转账完成]
        ```
    
        ## 违规详情
        ### 严重违规
        {os.linesep.join(f'🚨 `{v}`' for v in violation_analysis['critical']['examples']) if violation_analysis['critical']['count'] > 0 else '✅ 无'}
    
        ### 警告事项
        {os.linesep.join(f'⚠️ `{v}`' for v in violation_analysis['warning']['examples']) if violation_analysis['warning']['count'] > 0 else '✅ 无'}
    
        ## 智能建议
        1. {self._get_priority_suggestion(violation_analysis)}
        2. {self._get_process_suggestion(violations)}
        3. {self._get_knowledge_graph_suggestion(violations)}
        """
        # return os.path.abspath(report_path)
        return report_content


# --------------------- 示例运行 ---------------------
if __name__ == "__main__":

    # 新增数据库重置 ▼▼▼
    from py2neo import Graph
    neo4j_graph = Graph(os.getenv("NEO4J_URI", "bolt://localhost:7687"), auth=('neo4j', "12345678"))
    neo4j_graph.run("MATCH (n) DETACH DELETE n")

    # 初始化知识库
    kb = EnhancedKnowledgeBase()

    # --- 新增：PDF 文件加载和处理部分 ---
    # !!!请务必修改为您的实际PDF文件路径!!!
    pdf_file_path = "D:/trae_pro/AI审批流程测试/AI审批流程测试/ceshi.pdf" # <-- 在这里指定你的PDF文件路径

    # 您也可以提供多个PDF文件
    # pdf_file_paths = [
    #     "D:/Path/To/Your/Contract.pdf",
    #     "D:/Path/To/Your/InvoicePolicy.pdf",
    #     "D:/Path/To/Your/ApprovalFlow.pdf",
    # ]

    # 检查文件是否存在
    if not os.path.exists(pdf_file_path):
        logging.error(f"PDF文件未找到: {pdf_file_path}")
        # 如果文件不存在，使用一个默认的文本策略继续
        policy_text_for_test = """
        根据公司采购与付款流程规范：
        1. 采购合同签署完成后（事件:合同签署），必须进行供应商资质审核（事件:供应商资质审核）。关系: SEQUENCE
        2. 供应商资质审核通过后（事件:资质审核通过），方可进行货物验收（事件:货物验收）。关系: CONDITION
        3. 货物验收合格后（事件:验收合格），均可收到供应商开具的发票（事件:发票收到）。关系: SEQUENCE
        4. 发票收到后（事件:发票收到），财务部门需要进行发票验证（事件:发票验证）。关系: SEQUENCE
        5. 发票验证通过（事件:发票验证通过）是执行付款操作（事件:付款操作）的必要前提。关系: CONDITION
        6. 付款操作（事件:付款操作）后，银行执行转账（事件:银行转账完成）。关系: SEQUENCE
        7. 任何付款操作（事件:付款操作）都必须有前置的采购合同签署（事件:合同签署）。关系: CONDITION, CAUSES
        """
        logging.info("使用默认政策文本进行测试，因为PDF文件未找到。")
        kb.add_policy_document(policy_text_for_test)
    else:
        logging.info(f"正在加载并处理PDF文件: {pdf_file_path}")
        try:
            # loader = PyPDFLoader(pdf_file_path)
            # documents = loader.load() # 加载PDF中的所有页面作为Document对象
            # text_splitter = RecursiveCharacterTextSplitter(
            #     chunk_size=2000, # 每个文本块的最大长度，根据您的需求调整
            #     chunk_overlap=200, # 块之间的重叠，有助于保留上下文
            #     length_function=len,
            #     add_start_index=True
            # )
            # split_documents = text_splitter.split_documents(documents)

            # 替换原有的PyPDFLoader逻辑
            raw_text = kb._parse_pdf(pdf_file_path)
            # 使用改进的分割策略
            text_splitter = RecursiveCharacterTextSplitter(
                chunk_size=2500,
                chunk_overlap=400,
                separators=["\n\n第[一二三四五六七八九十]+章", "\n•\t"]
            )
            split_documents = text_splitter.create_documents([raw_text])

            # 将每个文本块的内容传递给知识库
            for i, doc_chunk in enumerate(split_documents):
                logging.info(f"正在处理文档块 {i+1}/{len(split_documents)} (来源: {doc_chunk.metadata.get('source', '未知')}, 页码: {doc_chunk.metadata.get('page', '未知')})...")
                kb.add_policy_document(doc_chunk.page_content)

            logging.info("所有PDF文件内容已加载并处理完毕。")

        except Exception as e:
            logging.error(f"处理PDF文件时发生错误: {e}")
            logging.error("将使用默认政策文本进行测试，以继续执行。")
            policy_text_for_test = """
            根据公司采购与付款流程规范：
            1. 采购合同签署完成后（事件:合同签署），必须进行供应商资质审核（事件:供应商资质审核）。关系: SEQUENCE
            2. 供应商资质审核通过后（事件:资质审核通过），均可接受货物验收（事件:货物验收）。关系: CONDITION
            3. 货物验收合格后（事件:验收合格），均可收到供应商开具的发票（事件:发票收到）。关系: SEQUENCE
            4. 发票收到后（事件:发票收到），财务部门需要进行发票验证（事件:发票验证）。关系: SEQUENCE
            5. 发票验证通过（事件:发票验证通过）是执行付款操作（事件:付款操作）的必要前提。关系: CONDITION
            6. 付款操作（事件:付款操作）后，银行执行转账（事件:银行转账完成）。关系: SEQUENCE
            7. 任何付款操作（事件:付款操作）都必须有前置的采购合同签署（事件:合同签署）。关系: CONDITION, CAUSES
            """
            kb.add_policy_document(policy_text_for_test)


    # --- 后续的流程验证部分 ---
    # 模拟一个业务流程步骤序列
    workflow_steps = [
        "合同签署",
        "供应商资质审核",
        "发票验证", # 这里故意用“发票验证”而非“发票验证通过”来测试匹配鲁棒性
        "付款操作",
        "银行转账完成"
    ]
    # 另一个测试案例（可能违规）：
    # workflow_steps = [
    #     "合同签署",
    #     "付款操作", # 缺少资质审核、发票验证、货物验收等
    #     "银行转账完成"
    # ]


    # logging.info(f"\n正在验证业务流程: {workflow_steps}")
    violations, validation_report = kb.validate_workflow(workflow_steps)

    # --------------------- 生成审计报告 ---------------------
    logging.info("\n--- 审计报告 ---")
    if violations:
        logging.warning("发现以下违规：")
        for violation in violations:
            logging.warning(violation)
    else:
        logging.info("未发现违规。")

    logging.info("\n--- 流程验证摘要 ---")
    logging.info(validation_report)

    # 生成审计报告
    report_path = kb.generate_audit_report(violations, validation_report, output_format="pdf")
    logging.info(f"已生成审计报告: {report_path}")

    # 可选：可视化图谱
    # logging.info("\n生成知识图谱可视化...")
    # kb.visualize_graph()